<template>
  <div>
    <h1>用户中心</h1>
    <i class="el-icon-loading"></i>
    <div ref="drag" id="drag">
      <input type="file" name="file" @change="handleFileChange" />
    </div>

    <div>
      <el-progress
        :stroke-width="20"
        :text-inside="true"
        :percentage="uploadProgress"
      ></el-progress>
    </div>

    <div>
      <p>计算hash的进度：</p>
      <el-progress
        :stroke-width="20"
        :text-inside="true"
        :percentage="hashProgress"
      ></el-progress>
    </div>

    <div>
      <el-button @click="uploadFile" :disabled="!this.file">上传</el-button>
    </div>

    <!-- hash进度方块 -->
    <div>
      <div class="cube-container" :style="{ width: this.cubeWidth + 'px' }">
        <div class="cube" v-for="chunk in chunks" :key="chunk.name">
          <div
            :class="{
              uploading: chunk.progress > 0 && chunk.progress < 100,
              success: chunk.progress == 100,
              error: chunk.progress < 0,
            }"
            :style="{ height: chunk.progress + '%' }"
          >
            <i
              class="el-icon-loading"
              style="color: #f56c6c; position: absolute"
              v-if="chunk.progress < 100 && chunk.progress > 0"
            ></i>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import spakMD5 from "spark-md5";
const CHUNK_SIZE = 1 * 1024 * 1024;
export default {
  async mounted() {
    const ret = await this.$http.get("/user/info");
    this.bindEvents();
  },
  data() {
    return {
      file: null,
      // uploadProgress: 0,
      chunks: [],
      hashProgress: 0,
    };
  },
  computed: {
    cubeWidth() {
      return Math.ceil(Math.sqrt(this.chunks.length)) * 16;
    },
    uploadProgress() {
      if (!this.file || this.chunks.length) {
        return 0;
      }
      const loaded = this.chunks
        .map((item) => item.chunk.size * item.progress)
        .reduce((acc, cur) => acc + cur, 0);
      return parseInt(((loaded * 100) / this.file.size).toFixed(2));
    },
  },
  methods: {
    bindEvents() {
      const drag = this.$refs.drag;
      drag.addEventListener("dragover", (e) => {
        drag.style.borderColor = "red";
        e.preventDefault();
      });
      drag.addEventListener("dragleave", (e) => {
        drag.style.borderColor = "#eee";
        e.preventDefault();
      });
      drag.addEventListener("drop", (e) => {
        const fileList = e.dataTransfer.files;
        this.file = fileList[0];
        drag.style.borderColor = "#eee";
        e.preventDefault();
      });
    },
    createFileChunk(file, size = CHUNK_SIZE) {
      const chunks = [];
      let cur = 0;
      while (cur < file.size) {
        chunks.push({ index: cur, file: file.slice(cur, cur + size) });
        cur += size;
      }
      return chunks;
    },
    caculateHashWorker() {
      return new Promise((resolve) => {
        this.worker = new Worker("/hash.js");
        this.worker.postMessage({ chunks: this.chunks });
        this.worker.onmessage = (e) => {
          const { progress, hash } = e.data;
          this.hashProgress = Number(progress.toFixed(2));
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    caculateHashIdel() {
      const chunks = this.chunks;
      return new Promise((resolve) => {
        const spark = new spakMD5.ArrayBuffer();
        let count = 0;

        const appendToSpark = async (file) => {
          return new Promise((resolve) => {
            const reader = new FileReader();
            reader.readAsArrayBuffer(file);
            reader.onload = (e) => {
              spark.append(e.target.result);
              resolve();
            };
          });
        };
        const workLoop = async (deadline) => {
          console.log(111);
          while (count < chunks.length && deadline.timeRemaining() > 1) {
            await appendToSpark(chunks[count].file);
            count++;
            if (count < chunks.length) {
              this.hashProgress = Number(
                ((100 * count) / chunks.length).toFixed(2)
              );
            } else {
              this.hashProgress = 100;
              resolve(spark.end());
            }
          }
          window.requestIdleCallback(workLoop);
        };
        window.requestIdleCallback(workLoop);
      });
    },
    async caculateHashSample() {
      return new Promise((resolve) => {
        const spark = new spakMD5.ArrayBuffer();
        const reader = new FileReader();

        const file = this.file;
        const size = file.size;
        const offset = 2 * 1024 * 1024;
        //第一个2M，最后一个区块数据全量，中间去前中后各两个字节
        let chunks = [file.slice(0, offset)];

        let cur = offset;
        while (cur < size) {
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset));
          } else {
            const mid = cur + offset / 2;
            const end = cur + offset;
            chunks.push(file.slice(cur, cur + 2));
            chunks.push(file.slice(mid, mid + 2));
            chunks.push(file.slice(end, end - 2));
          }
          cur += offset;
        }
        reader.readAsArrayBuffer(new Blob(chunks));
        reader.onload = (e) => {
          spark.append(e.target.result);
          this.hashProgress = 100;
          resolve(spark.end());
        };
      });
    },
    async uploadFile() {
      // if (!(await this.isImage(this.file))) {
      //   alert("文件格式不对");
      //   return;
      // }
      const chunks = this.createFileChunk(this.file);
      // 计算文件哈希
      // 方式一
      // const hash = await this.caculateHashWorker();
      // 方式二
      // const hash1 = await this.caculateHashIdel();
      // 方式三
      const hash2 = await this.caculateHashSample();
      // console.log("文件hash", hash);
      // console.log("文件hash1", hash1);
      // console.log("文件hash2", hash2);
      this.hash = hash2;

      const {
        data: { uploaded, uploadedList },
      } = await this.$http.post("/checkfile", {
        hash: this.hash,
        ext: this.file.name.split(".").pop(),
      });

      if (uploaded) {
        return this.$message.success("秒传成功!");
      }

      this.chunks = chunks.map((chunk, index) => {
        const name = hash2 + "-" + index;
        return {
          hash: hash2,
          name,
          index,
          chunk: chunk.file,
          progress: uploadedList.indexOf(name) > -1 ? 100 : 0,
        };
      });

      await this.uploadChunks(uploadedList);
    },

    async uploadChunks(uploadedList) {
      const requests = this.chunks
        .filter((chunk) => uploadedList.indexOf(chunk.name) === -1) // 过滤掉已经上传的chunk
        .map((chunk, index) => {
          const form = new FormData();
          form.append("chunk", chunk.chunk);
          form.append("hash", chunk.hash);
          form.append("name", chunk.name);
          // form.append("index", chunk.index);
          return { form, index: chunk.index, error: 0 };
        });
      // .map(({ form, index }) =>
      //   this.$http.post("/uploadfile", form, {
      //     onUploadProgress: (progress) => {
      //       this.chunks[index].progress = Number(
      //         ((progress.loaded / progress.total) * 100).toFixed(2)
      //       );
      //     },
      //   })
      // );

      // await Promise.all(requests);
      await this.sendRequest(requests);

      await this.mergeRequest();

      // const form = new FormData();
      // form.append("name", "file");
      // form.append("file", this.file);
      // const ret = await this.$http.post("uploadfile", form, {
      //   onUploadProgress: (progress) => {
      //     this.uploadProgress = Number(
      //       ((progress.loaded / progress.total) * 100).toFixed(2)
      //     );
      //   },
      // });
    },
    async sendRequest(chunks, limit = 4) {
      return new Promise((resolve, reject) => {
        const len = chunks.length;
        let count = 0;
        let isStop = false;
        const start = async () => {
          if (isStop) return;
          const task = chunks.shift();
          if (task) {
            const { form, index } = task;
            try {
              await this.$http.post("/uploadfile", form, {
                onUploadProgress: (progress) => {
                  this.chunks[index].progress = Number(
                    ((progress.loaded / progress.total) * 100).toFixed(2)
                  );
                },
              });
              if (count === len - 1) {
                resolve();
              } else {
                count++;
                start();
              }
            } catch (e) {
              this.chunks[index].progress = -1;
              if (task.error < 3) {
                task.error++;
                chunks.unshift(task);
                start();
              } else {
                isStop = true;
                reject();
              }
            }
          }
        };
        while (limit > 0) {
          start();
          limit -= 1;
        }
      });
    },
    async mergeRequest() {
      this.$http.post("mergefile", {
        ext: this.file.name.split(".").pop(),
        size: CHUNK_SIZE,
        hash: this.hash,
      });
    },
    async blobToString(blob) {
      return new Promise((resolve) => {
        const reader = new FileReader();
        reader.onload = () => {
          const ret = reader.result
            .split("")
            .map((v) => v.charCodeAt())
            .map((v) => v.toString(16).toUpperCase())
            .map((v) => v.padStart(2, "0"))
            .join(" ");
          resolve(ret);
        };
        reader.readAsBinaryString(blob);
      });
    },
    async isPng(file) {
      const ret = await this.blobToString(file.slice(0, 8));
      const isPng = ret === "89 50 4E 47 0D 0A 1A 0A";
      return isPng;
    },
    async isGif(file) {
      // GIF98a 和 GIF87a
      const ret = await this.blobToString(file.slice(0, 6));
      const isGif = ret === "47 49 46 38 39 61" || ret === "47 49 46 38 37 61";
      return isGif;
    },
    async isJpg(file) {
      const len = file.size;
      const start = await this.blobToString(file.slice(0, 2));
      const tail = await this.blobToString(file.slice(-2, len));
      const isJpg = start === "FF 08" && tail === "FF D9";
      return isJpg;
    },
    async isImage(file) {
      return (
        (await this.isGif(file)) ||
        (await this.isPng(file)) ||
        (await this.isJpg(file))
      );
    },
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      this.file = file;
    },
  },
};
</script>

<style lang="stylus">
#drag {
  height: 100px;
  line-height: 100px;
  border: 1px dashed #eee;
  text-align: center;
  vertical-align: middle;
}

.cube-container {
  .cube {
    width: 14px;
    height: 14px;
    line-height: 12;
    border: 1px black solid;
    background: #eee;
    float: left;
    position: relative;

    >.success {
      background: green;
    }

    >.uploading {
      background: blue;
    }

    >.error {
      background: red;
    }
  }
}
</style>